<!DOCTYPE html>
<html>

<head>
    <script>
        // set year, then open with chrome and print to A4 paper
        var year = 2021;
    </script>
    <title>Calender</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="apple-mobile-web-app-capable" content="yes">
    <style type="text/css">
        @media print {
            @page {
                size: A4 portrait;
            }
        }

        body {
            font-family: Monospace;
        }

        img {
            width: 128px;
            height: 128px;
            display: inline-block;
            margin-right: 10px;
        }

        .year {
            font-weight: bold;
            font-size: 32px;
            text-align: center;
            margin-top: 20px;
        }

        .month {
            margin: 16px 10px;
            float: left;
        }

        .month-num {
            font-weight: bold;
            margin-left: 8px;
        }

        table {
            border-collapse: collapse;
        }

        table,
        th,
        td {
            /* border: 1px solid blue; */
            text-align: center;
        }

        td {
            width: 28px;
            height: 26px;
        }

        .week {
            font-size: 10px;
        }

        .week td {
            height: 20px;
        }

        .day {
            font-weight: bold;
            line-height: 16px;
            font-size: 12px;
            margin: 0;
        }

        .lunal {
            font-size: 6px;
            line-height: 6px;
            margin: 0;
        }

        .lunal-first {
            font-weight: bold;
        }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <!-- 农历是香港天文台观测出来的：https://www.hko.gov.hk/tc/gts/time/conversion1_text.htm -->
    <!-- 还可以参考 https://zhuanlan.zhihu.com/p/57261062 -->
    <script>
        'use strict';

        /**
         * @auth iilu.me
         * 用法说明可查看 [lunarFun](https://github.com/iilu/lunarFun)
         * 没有特殊说明，牵扯到的数字日期都为中国公历日期，农历日期会特别说明
         */

        const LUNAR_INFO = {
            MIN_YEAR: 1901, // YEAR_INFO 的最小年份，和数组下标配合可知每个项的年份
            MAX_YEAR: 2100, // YEAR_INFO 的最大年份
            YEAR_INFO: [
                "4e4ae0", "22a570", "7554d5", "42d260", "12d950", "e56554", "3656a0", "a9ad0", "5955d2", "2a4ae0",
                "79a5b6", "4aa4d0", "1ad250", "e9d255", "3ab540", "ed6a0", "5dada2", "2e95b0", "864977", "524970",
                "22a4b0", "71b4b5", "426a50", "166d40", "e1ab54", "362b60", "a9570", "5d52f2", "2a4970", "796566",
                "46d4a0", "1aea50", "e96a95", "3a5ad0", "122b60", "e186e3", "2e92e0", "fd48d7", "4ec950", "22d4a0",
                "edd8a6", "3eb550", "1656a0", "e5a5b4", "3625d0", "a92d0", "59d2b2", "2aa950", "75b557", "466ca0",
                "1ab550", "ed5355", "3a4da0", "ea5b0", "e14573", "3252b0", "7da9a8", "4ae930", "226aa0", "71aea6",
                "3eab50", "164b60", "65aae4", "36a570", "a5260", "55f263", "26d950", "795b57", "4656a0", "1a96d0",
                "6d4dd5", "3e4ad0", "ea4d0", "5dd4d4", "2ed250", "7dd558", "4ab540", "1eb6a0", "f195a6", "4295b0",
                "1649b0", "65a974", "36a4b0", "ab27a", "526a50", "266d40", "75af46", "46ab60", "1a9570", "6d4af5",
                "3e4970", "1264b0", "5d74a3", "2aea50", "7d6b58", "4e5ac0", "1eab60", "7196d5", "4292e0", "16c960",
                "61d954", "32d4a0", "6da50", "597552", "2656a0", "75abb7", "4a25d0", "1e92d0", "69cab5", "3aa950",
                "eb4a0", "5dbaa4", "2aad50", "7d55d9", "4e4ba0", "22a5b0", "f15176", "4252b0", "16a930", "657954",
                "326aa0", "6ad50", "595b52", "2a4b60", "75a6e6", "46a4e0", "1ad260", "69ea65", "36d530", "e5aa0",
                "5d76a3", "2e96d0", "7d4afb", "4e4ad0", "22a4d0", "f1d0b6", "3ed250", "12d520", "61dd45", "32b5a0",
                "656d0", "5955b2", "2a49b0", "79a577", "46a4b0", "1aaa50", "e9b255", "3a6d20", "aada0", "dd4b63",
                "2e9370", "649f8", "4e4970", "2264b0", "f168a6", "3eea50", "126aa0", "e1a6c4", "32aae0", "a92e0",
                "55d2e3", "26c960", "75d557", "46d4a0", "16da50", "695d55", "3a56a0", "ea6d0", "5d55d4", "2e52d0",
                "7da9b8", "4ea950", "1eb4a0", "6db6a6", "3ead50", "1655a0", "61aba4", "32a5b0", "a52b0", "59b273",
                "266930", "757337", "466aa0", "1aad50", "e94b55", "3a4b60", "ea570", "6154e4", "2ad160", "79e968",
                "4ad520", "1edaa0", "ed6aa6", "3e56d0", "164ae0", "65a9d4", "32a2d0", "6d150", "55f252", "26d520"
            ],
            HEAVENLY_STEMS: ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'],
            EARTHLY_BRANCHES: ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'],
            ZODIAC: ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'],
            CHINESE_MONTH: ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'],
            CHINESE_DATE: ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '初', '廿', '卅', '年', '月', '日', '闰'], // 廿: nian; 卅: sa; 都读四声
            CHINESE_SOLAR_TERMS: ['立春', '雨水', '惊蛰', '春分', '清明', '谷雨', '立夏', '小满', '芒种', '夏至', '小暑', '大暑', '立秋', '处暑', '白露', '秋分', '寒露', '霜降', '立冬', '小雪', '大雪', '冬至', '小寒', '大寒']
        }

        /**
         * _ 开头的方法默认为私有方法
         */
        class LunarFunClass {
            constructor() {

            }

            /**
             * 作为下列方法必传参数的默认参数传入
             * 用来在方法所需的参数没有传入的时候抛出错误
             */
            _missingParameters() {
                throw new Error('Missing parameter');
            }

            /**
             * 传入中国农历年份和其对应的十六进制字符串信息，返回JS对象表示的农历数据
             * 传入的十六进制字符串不包括 “0x” 前缀
             * @param { number } year
             * @param { string } numStr
             */
            toJSON(year = this._missingParameters(), numStr = this._missingParameters()) {
                if (!numStr) {
                    return '';
                }
                let hexadecimal = numStr.toString('16'); // 保证使用的数据是十六进制的字符串
                let binary = parseInt(hexadecimal, 16).toString(2);
                if (binary.length !== 24) { // 不足24位则左边补0补足24位
                    binary = '0'.repeat(24 - binary.length) + binary;
                }
                let lunarItem = {
                    'year': +year,
                };
                // binary 第 21-24 个字符判断是否是闰年，如果是则得到闰几月
                // binary 第 1 个字符是当年份是闰年的时候判断闰月的天数，1是大月30天， 0是小月29天，如果不是闰年则为0
                let runInfo = binary.slice(-4);
                if (runInfo === '0000') {
                    lunarItem.isRun = false;
                    lunarItem.runMonth = 0;
                    lunarItem.runMonthDays = 0;
                } else {
                    lunarItem.isRun = true;
                    lunarItem.runMonth = parseInt(runInfo, 2);
                    lunarItem.runMonthDays = +binary.slice(0, 1) + 29;
                }

                // binary 第 9-20 个字符是当年的正常月份天数，1是大月30天， 0是小月29天
                lunarItem.monthsDays = [];
                let monthInfo = binary.slice(8, 20);
                [...monthInfo].map(item => {
                    lunarItem.monthsDays.push(+item + 29);
                })

                // binary 第 7-8 个字符是 农历年份正月初一对应的公历月份
                lunarItem.firstMonth = parseInt(binary.slice(6, 8), 2);

                // binary 2-6 个字符是 农历年份正月初一对应的公历日子
                lunarItem.firstDay = parseInt(binary.slice(1, 6), 2);

                return lunarItem;
                /* 输出例子
                    {
                         "year": 2000, // 农历年份的数字表示
                         "isRun": false, // 是否是闰年
                         "runMonth": 0, // 是闰年的话闰几月，非闰年为 0
                         "runMonthDays": 0, // 是闰年的话闰月的天数，非闰年为 0
                         "monthsDays": [30, 30, 29, 29, 30, 29, 29, 30, 29, 30, 30, 29], // 正常十二个月的每月天数
                         "firstMonth": 2, // 农历年份正月初一对应的公历月份
                         "firstDay": 5 // 农历年份正月初一对应的公历日子
                    }
                */
            }

            /**
            * 输入两个公历日期对象，输出两个日期间隔的天数
            * 注意，请传入两个时区相同的对象，例如传入的两个日起对象都是 UTC 时间，或都是中国时区的日起对象
            * @param date1
            * @param date2
            * @returns {number}
            */
            distanceDate(date1 = this._missingParameters(), date2 = this._missingParameters()) {
                let distance = date1 - date2; // 以毫秒计的运行时长

                // 这里会有精度问题，由原来的 Math.floor 转为 Math.round 方法，因为计算的天数有可能会出现 在中国时区为 354天，在新加坡时区为 353.99天，（中国时区和新加坡时区应该都是 +8 时区，按理说获取的时间戳是一样的，但它就是有误差我也是没办法
                // return Math.floor(Math.abs(distance) / 1000 / 60 / 60 / 24); // 相差的毫秒数转为天数
                return Math.round(Math.abs(distance) / 1000 / 60 / 60 / 24); // 相差的毫秒数转为天数
            }

            /**
             * 输入中国公历年月日，返回其中国时间日期对象，时分秒不传默认 0
             * 注意：此日期对象最好不要使用日期对象的获取年月日方法等直接返回年月日，因为时区不一定是,在中国
             * 因为月份在日期对象里面需要 -1(js日期对象月份从0开始 0-11)，时常忘记，所以这里写一个方法
             * @param year
             * @param month
             * @param day
             * @returns {Date}
             */
            getDateYMD(year = this._missingParameters(), month = this._missingParameters(), day = this._missingParameters(), h = 0, m = 0, s = 0) {
                if (new Date().getTimezoneOffset() === -480) { // 表示当前是在中国时区
                    return new Date(+year, month - 1, +day, h, m, s);

                } else { // 表示当前不是在中国时区，那么根据输入的年月日，返回其中国时区相同的日期对象(即时间戳一样)
                    let localDate = new Date(+year, month - 1, +day, h, m, s); // 本地时间
                    let offsetGMT = new Date().getTimezoneOffset(); // 本地时间和格林威治的时间差，单位为分钟
                    let chinaTimestamp = localDate.getTime() - (offsetGMT * 60 * 1000) - (8 * 60 * 60 * 1000); // 中国时间戳

                    return new Date(chinaTimestamp);
                }
            }

            /**
             * 传入中国公历年月日返回农历年月日数组
             * 数组的第四个项是在年份为闰年的时候决定输出的月份是否是闰月
             * @param { number } year 
             * @param { number } month 
             * @param { number } day 
             */
            gregorianToLunal(year = this._missingParameters(), month = this._missingParameters(), day = this._missingParameters()) {
                let yearData = this.LUNAR_INFO.YEAR_INFO[year - this.LUNAR_INFO.MIN_YEAR]; // 获取输入年份的16进制数据
                let yearDataInfo = this.toJSON(+year, yearData); // 转化为 JSON数据

                /**
                 * 以输入年份的农历正月初一对应的月份和天数来作为基准
                 * 如果输入的月份和天数比基准 大，说明输出的农历年份是同年
                 * 如果输入的月份和天数比基准 小，说明输出的农历年份要比同年公历小一年
                 */
                let compare = 0; // 0 表示输入的月份天数和基准一致, 即正月初一 || 1 表示输入的月份天数比基准 大 || -1 表示输入的月份天数比基准 小
                if (month > yearDataInfo.firstMonth) { // 输入的月份和比正月初一对应的月份大，
                    compare = 1;
                } else if (month < yearDataInfo.firstMonth) { // 输入的月份比正月初一对应的月份小
                    compare = -1;
                } else if (+month === yearDataInfo.firstMonth) { // 输入的月份和正月初一的月份相同，这个时候比较天数
                    if (day > yearDataInfo.firstDay) {
                        compare = 1;
                    } else if (day < yearDataInfo.firstDay) {
                        compare = -1;
                    } else if (+day === yearDataInfo.firstDay) {
                        compare = 0;
                    }
                }

                let lunalYear = 0; // 输出的农历年份
                let lunalMonth = 0; // 输出的农历月份
                let lunalDay = 0; // 输出的农历天
                let lunalIsRun = false; // 如果输出的农历年份是闰年，此参数有效，判断输出的月份是否是闰月， 默认false

                if (compare === 1) { // 输入的月份天数比基准 大， 使用同输入年份的农历年份数据
                    lunalYear = +year;
                } else if (compare === -1) { // 输入的月份天数比基准 小，使用同输入年份 上一年 的农历数据
                    lunalYear = year - 1;
                    yearData = this.LUNAR_INFO.YEAR_INFO[lunalYear - this.LUNAR_INFO.MIN_YEAR];
                    yearDataInfo = this.toJSON(lunalYear, yearData);
                } else if (compare === 0) { // 输入的月份天数和基准一致
                    lunalYear = +year;
                    lunalMonth = 1;
                    lunalDay = 1;
                }

                let differDays = this.distanceDate(this.getDateYMD(lunalYear, yearDataInfo.firstMonth, yearDataInfo.firstDay), this.getDateYMD(year, month, day)); // 输入的公历年月日和其所在农历正月初一相差的天数
                let monthsTotalArr = [...yearDataInfo.monthsDays]; // 农历所有月份组成的数组，包括闰月，
                if (yearDataInfo.isRun) { // 如果有闰月，则在原来的月份后面插入闰月
                    monthsTotalArr.splice(yearDataInfo.runMonth, 0, yearDataInfo.runMonthDays);
                }
                let monthsTotalArrIndex = 0; // 农历月份处在monthsTotalArr的下标
                let reduceBreak = false; // 额外参数，用来判断是否还进行计算 monthsTotalArrIndex 和 lunalDay
                monthsTotalArr.reduce((accumulator, currentValue, index) => {
                    // 与正月初一相差的天数+1即是正月初一到输入年月日的天数，例如1号到3号相差2天，但1号到3号总共3天
                    if (((accumulator + currentValue) >= (differDays + 1)) && !reduceBreak) {
                        reduceBreak = true;
                        monthsTotalArrIndex = index;
                        // 天数 = 月份的天数 - (农历前几个月的总和 - (公历相差天数 + 1))
                        lunalDay = monthsTotalArr[index] - ((accumulator + currentValue) - (differDays + 1));
                    }
                    return accumulator + currentValue
                }, 0);

                /**
                 * monthsTotalArrIndex 是所有农历月份数组的下标，如果不是闰年 monthsTotalArrIndex+1 就表示当前农历月份
                 * 如果是闰年，monthsTotalArrIndex+1 <= 闰月， 则 monthsTotalArrIndex+1 表示当前农历月份
                 * 如果是闰年，monthsTotalArrIndex+1 > 闰月，则 monthsTotalArrIndex 表示当前农历月份
                 * 如果 monthsTotalArrIndex+1 刚好比闰月打一个月，则 lunalIsRun 为 true
                 */
                if (yearDataInfo.isRun) {
                    if ((monthsTotalArrIndex + 1) <= yearDataInfo.runMonth) {
                        lunalMonth = monthsTotalArrIndex + 1;
                    } else {
                        lunalMonth = monthsTotalArrIndex;
                        // (monthsTotalArrIndex + 1) - yearDataInfo.runMonth === 1 缩写一下就是下面的判断
                        if (monthsTotalArrIndex === yearDataInfo.runMonth) {
                            lunalIsRun = true
                        }
                    }
                } else {
                    lunalMonth = monthsTotalArrIndex + 1;
                }

                return [+lunalYear, +lunalMonth, +lunalDay, lunalIsRun];
            }
        }

        LunarFunClass.prototype.LUNAR_INFO = LUNAR_INFO;

        (function (global, factory) {
            typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
                typeof define === 'function' && define.amd ? define(factory) :
                    (global.lunarFun = factory()); // lunarFun 是库的名字
        }(this, (function () {
            'use strict';
            return new LunarFunClass();
        })));
    </script>
</head>

<body>

    <div id="app">
        <div class="year">{{ year }}</div>
        <div class="month" v-for="m in calender">
            <table>
                <div><span class="month-num">{{ m.num }}</span>月</div>
                <tr class="week">
                    <td>一</td>
                    <td>二</td>
                    <td>三</td>
                    <td>四</td>
                    <td>五</td>
                    <td>六</td>
                    <td>日</td>
                </tr>
                <tr v-for="week in m.weeks">
                    <td v-for="d in week">
                        <p class="day">{{ d == null ? '' : d.num }}</p>
                        <p class="lunal"
                            :style="{ textDecoration: (d != null && d.lunal.isFirst) ? 'underline' : 'none' }">
                            {{ d == null ? '' : d.lunal.lunalDay }}</p>
                    </td>
                </tr>
            </table>
        </div>

    </div>
    <script type="text/javascript">
        var app = new Vue({
            el: '#app',
            data: {
                calender: null,
                year: year
            }
        })

        const CHINESE_MONTH = ['', '正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];
        const CHINESE_DATE = ['十', '一', '二', '三', '四', '五', '六', '七', '八', '九', '初', '十', '廿', '三'];

        function getLunalStr(year, month, day) {
            info = lunarFun.gregorianToLunal(year, month, day);
            let d = info[2];
            let m = info[1];
            ret = {
                isFirst: false,
                lunalDay: ''
            }
            if (d == 1) {
                ret.isFirst = true;
                ret.lunalDay = (info[3] ? '闰' : '') + CHINESE_MONTH[m] + '月';
            } else {
                if (d <= 10) {
                    ret.lunalDay = '初' + CHINESE_DATE[d % 10];
                } else {
                    ret.lunalDay = CHINESE_DATE[parseInt(d / 10) + 10] + CHINESE_DATE[d % 10];
                }
            }

            return ret;
        }

        function makeYear() {
            let calender = [];

            for (let m = 0; m < 12; ++m) {
                let month = {
                    num: m + 1,
                    weeks: []
                };
                let dn = new Date(year, m + 1, 0).getDate();
                let fw = new Date(year, m, 1).getDay();
                fw = fw == 0 ? 7 : fw;

                let week = [];
                for (let n = 1; n < fw; ++n) {
                    week.push(null);
                }
                for (let d = 1; d <= dn; ++d) {
                    week.push({
                        num: d,
                        lunal: getLunalStr(year, m + 1, d)
                    })
                    if (week.length == 7) {
                        month.weeks.push(week);
                        week = [];
                    }
                }
                if (week.length > 0) {
                    month.weeks.push(week);
                }

                while (month.weeks.length < 6) {
                    month.weeks.push([null]);
                }

                calender.push(month);
            }

            return calender;
        }

        app.calender = makeYear();
    </script>

</body>

</html>